package tasknow.storage;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Calendar;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import tasknow.ui.DisplayAndCommandTextComposite;
import tasknow.utils.Constants;
import tasknow.utils.Task;

public class Storage {

	// All class written by A0018386X
	// @author A0118386X

	// Constants--------------------------------------------------------

	private final String START_CALENDAR_TAG = "START_CALENDAR";
	private final String END_CALENDAR_TAG = "END_CALENDAR";
	private final String DESCRIPTION_TAG = "DESCRIPTION";
	private final String YEAR_TAG = "YEAR";
	private final String MONTH_TAG = "MONTH";
	private final String DAY_TAG = "DAY";
	private final String HOUR_TAG = "HOUR";
	private final String MINUTE_TAG = "MINUTE";
	private final String STATUS_TAG = "STATUS";
	private final String TASK_CATALOUGE_TAG = "TASK_CATALOUGE";
	private final String TASK_TAG = "TASK";
	private final String XML_EXTENSION = ".xml";
	private final String ERROR_INVALID_FILE_NAME = "No valid file name found.";
	private final String ERROR_UNABLE_TO_READ_FILE = "Unable to read file.";
	private final String MSG_FAILED_TO_PARSE_XML_FILE = "Failed to parse XML file.";
	private final String MSG_FAILED_TO_CONSTRUCT_TRANSFORMER = "Failed to construct Transformer.";
	private final String MSG_FAILED_TO_CONSTRUCT_DOCUMENT_BUILDER = "Failed to construct document reader.";
	private final String MSG_INVALID_CALENDAR_VALUE = "Invalid Calender value found.";
	private final String MSG_CALENDAR_VALUE_NON_NUMERICAL = "Non-numerical value found.";
	private final String MSG_INVALID_STATUS = "Invalid Status state found.";
	private final String INCOMPLETE_STATUS = "Incomplete";
	private final String COMPLETED_STATUS = "Completed ";
	private final String UNDUE_STATUS = " Overdue  ";

	// Instances--------------------------------------------------------

	private Document document = null;
	private Element rootElement = null;
	private String fileName = Constants.EMPTY_STRING;
	private Logger logger = Logger.getLogger("");

	// Constructor------------------------------------------------------

	public Storage(String fileName_) throws InvalidFileNameException{
		this.setFileName(fileName_);
	}

	// Public Methods---------------------------------------------------

	public Vector<Task> readFromFile() throws DamagedXmlFileException {
		Vector<Task> taskVector = new Vector<Task>();

		if (!isEmptyFile()) {
			createDocumentRead(fileName);
			NodeList nodeList = document.getElementsByTagName(TASK_TAG);
			populateTaskVector(taskVector, nodeList);
		}

		return taskVector;
	}

	public void saveToFile(Vector<Task> taskVector) {
		createDocumentWrite();
		createRoot();

		for (Task task : taskVector) {
			addOneTask(task);
		}

		writeToFile(fileName);
	}

	public String getFileName() {
		return fileName;
	}

	public void setFileName(String fileName_) throws InvalidFileNameException {
		// Add .xml if needed
		if (!fileName_.endsWith(XML_EXTENSION)) {
			fileName_ += XML_EXTENSION;
		}

		if (isValidFormat(fileName_)) {
			this.fileName = fileName_;
		} else {
			throw new InvalidFileNameException(ERROR_INVALID_FILE_NAME);
		}
	}

	// Private Methods---------------------------------------------

	private void populateTaskVector(Vector<Task> taskVector, NodeList nodeList) throws DamagedXmlFileException {
		for (int i = 0; i < nodeList.getLength(); i++) {
			Node node = nodeList.item(i);
			Task newTask = taskFromNode(node);

			taskVector.add(newTask);
		}
	}

	private Task taskFromNode(Node nNode) throws DamagedXmlFileException {
		Element eElement = (Element) nNode;
		Task newTask = new Task(Constants.EMPTY_STRING);
		
		addDescription(eElement, newTask);
		NodeList nodeList = nNode.getChildNodes();

		for (int i = 0; i < nodeList.getLength(); i++) {
			if (nodeList.item(i).getNodeName() == START_CALENDAR_TAG) {
				addStartCalendar(nodeList.item(i), newTask);
			}

			if (nodeList.item(i).getNodeName() == END_CALENDAR_TAG) {
				addEndCalendar(nodeList.item(i), newTask);
			}
		}

		addStatus(eElement, newTask);

		return newTask;
	}

	private void addDescription(Element eElement, Task newTask) {
		newTask.modifyDescription(eElement
				.getElementsByTagName(DESCRIPTION_TAG).item(0).getTextContent());
	}

	private void addStartCalendar(Node startCalendarNode, Task newTask) throws DamagedXmlFileException {
		Element eElement = (Element) startCalendarNode;

		try{
			Integer year = Integer.parseInt(eElement.getElementsByTagName(YEAR_TAG)
					.item(0).getTextContent());
			Integer month = Integer.parseInt(eElement
					.getElementsByTagName(MONTH_TAG).item(0).getTextContent());
			Integer date = Integer.parseInt(eElement.getElementsByTagName(DAY_TAG)
					.item(0).getTextContent());
			Integer hourOfDay = Integer.parseInt(eElement
					.getElementsByTagName(HOUR_TAG).item(0).getTextContent());
			Integer minute = Integer.parseInt(eElement
					.getElementsByTagName(MINUTE_TAG).item(0).getTextContent());

			if (Constraints.isValidCalendar(year, month, date, hourOfDay, minute)){
				Calendar startCalendar = Calendar.getInstance();

				startCalendar.set(year, month, date, hourOfDay, minute);
				newTask.modifyStartCalendar(startCalendar);
			} else {
				logger.log(Level.WARNING, MSG_INVALID_CALENDAR_VALUE);
				throw new DamagedXmlFileException(MSG_INVALID_CALENDAR_VALUE);
			}
		} catch (NumberFormatException e){
			logger.log(Level.WARNING, MSG_CALENDAR_VALUE_NON_NUMERICAL);
			throw new DamagedXmlFileException(MSG_CALENDAR_VALUE_NON_NUMERICAL);
		}
	}

	private void addEndCalendar(Node endCalendarNode, Task newTask) throws DamagedXmlFileException {
		Element eElement = (Element) endCalendarNode;

		try{
			Integer year = Integer.parseInt(eElement.getElementsByTagName(YEAR_TAG)
					.item(0).getTextContent());
			Integer month = Integer.parseInt(eElement
					.getElementsByTagName(MONTH_TAG).item(0).getTextContent());
			Integer date = Integer.parseInt(eElement.getElementsByTagName(DAY_TAG)
					.item(0).getTextContent());
			Integer hourOfDay = Integer.parseInt(eElement
					.getElementsByTagName(HOUR_TAG).item(0).getTextContent());
			Integer minute = Integer.parseInt(eElement
					.getElementsByTagName(MINUTE_TAG).item(0).getTextContent());

			if (Constraints.isValidCalendar(year, month, date, hourOfDay, minute)){
				Calendar endCalendar = Calendar.getInstance();

				endCalendar.set(year, month, date, hourOfDay, minute);
				newTask.modifyEndCalendar(endCalendar);
			} else {
				logger.log(Level.WARNING, MSG_INVALID_CALENDAR_VALUE);
				throw new DamagedXmlFileException(MSG_INVALID_CALENDAR_VALUE);
			}
		} catch (NumberFormatException e) {
			logger.log(Level.WARNING, MSG_CALENDAR_VALUE_NON_NUMERICAL);
			throw new DamagedXmlFileException(MSG_CALENDAR_VALUE_NON_NUMERICAL);
		}
	}

	private void addStatus(Element eElement, Task newTask) throws DamagedXmlFileException {
		if (eElement.getElementsByTagName(STATUS_TAG).item(0).getTextContent()
				.contains(COMPLETED_STATUS)) {
			newTask.mark();
		} else if (eElement.getElementsByTagName(STATUS_TAG).item(0).getTextContent()
				.contains(INCOMPLETE_STATUS)) {
		} else if (eElement.getElementsByTagName(STATUS_TAG).item(0).getTextContent()
				.contains(UNDUE_STATUS)) {
		} else {
			logger.log(Level.WARNING, MSG_INVALID_STATUS);
			throw new DamagedXmlFileException(MSG_INVALID_STATUS);
		}
	}

	private void createRoot() {
		rootElement = document.createElement(TASK_CATALOUGE_TAG);
		document.appendChild(rootElement);
	}

	private void createDocumentWrite() {
		DocumentBuilderFactory documentFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder documentBuilder;

		try {
			documentBuilder = documentFactory.newDocumentBuilder();
			document = documentBuilder.newDocument();
		} catch (ParserConfigurationException e) {
			logger.log(Level.WARNING, MSG_FAILED_TO_CONSTRUCT_DOCUMENT_BUILDER);
		}
	}

	private void createDocumentRead(String fileName_) throws DamagedXmlFileException {
		File xmlFile = new File(fileName_);
		DocumentBuilderFactory documentFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder documentBuilder;

		try {
			documentBuilder = documentFactory.newDocumentBuilder();
			document = documentBuilder.parse(xmlFile);
		} catch (ParserConfigurationException e) {
			logger.log(Level.WARNING, MSG_FAILED_TO_CONSTRUCT_DOCUMENT_BUILDER);
			throw new DamagedXmlFileException(MSG_FAILED_TO_CONSTRUCT_DOCUMENT_BUILDER);
		} catch (SAXException e) {
			logger.log(Level.WARNING, MSG_FAILED_TO_PARSE_XML_FILE);
			throw new DamagedXmlFileException(MSG_FAILED_TO_PARSE_XML_FILE);
		} catch (IOException e) {
			logger.log(Level.WARNING, MSG_FAILED_TO_PARSE_XML_FILE);
			throw new DamagedXmlFileException(MSG_FAILED_TO_PARSE_XML_FILE);
		}
	}

	private void writeToFile(String filename) {
		try {
			TransformerFactory transformerFactory = TransformerFactory
					.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource domSource = new DOMSource(document);
			StreamResult streamResult = new StreamResult(new File(filename));
			transformer.transform(domSource, streamResult);
		} catch (TransformerConfigurationException e) {
			logger.log(Level.WARNING, MSG_FAILED_TO_CONSTRUCT_TRANSFORMER);
			throw new RuntimeException(MSG_FAILED_TO_CONSTRUCT_TRANSFORMER);
		} catch (TransformerException e) {
			logger.log(Level.WARNING, MSG_FAILED_TO_CONSTRUCT_TRANSFORMER);
			throw new RuntimeException(MSG_FAILED_TO_CONSTRUCT_TRANSFORMER);
		}
	}

	private void addOneTask(Task task_) {
		Element task = document.createElement(TASK_TAG);

		rootElement.appendChild(task);
		addElement(task, DESCRIPTION_TAG, task_.getDescription());

		if (task_.getStartDate() != null) {
			Element startCalendar = document.createElement(START_CALENDAR_TAG);
			task.appendChild(startCalendar);

			addElement(startCalendar, YEAR_TAG,
					Integer.toString(task_.getStartDate().get(Calendar.YEAR)));
			addElement(startCalendar, MONTH_TAG,
					Integer.toString(task_.getStartDate().get(Calendar.MONTH)));
			addElement(
					startCalendar,
					DAY_TAG,
					Integer.toString(task_.getStartDate().get(
							Calendar.DAY_OF_MONTH)));
			addElement(
					startCalendar,
					HOUR_TAG,
					Integer.toString(task_.getStartDate().get(
							Calendar.HOUR_OF_DAY)));
			addElement(startCalendar, MINUTE_TAG,
					Integer.toString(task_.getStartDate().get(Calendar.MINUTE)));
		}

		if (task_.getEndDate() != null) {
			Element endCalendar = document.createElement(END_CALENDAR_TAG);
			task.appendChild(endCalendar);

			addElement(endCalendar, YEAR_TAG,
					Integer.toString(task_.getEndDate().get(Calendar.YEAR)));
			addElement(endCalendar, MONTH_TAG,
					Integer.toString(task_.getEndDate().get(Calendar.MONTH)));
			addElement(
					endCalendar,
					DAY_TAG,
					Integer.toString(task_.getEndDate().get(
							Calendar.DAY_OF_MONTH)));
			addElement(
					endCalendar,
					HOUR_TAG,
					Integer.toString(task_.getEndDate().get(
							Calendar.HOUR_OF_DAY)));
			addElement(endCalendar, MINUTE_TAG,
					Integer.toString(task_.getEndDate().get(Calendar.MINUTE)));
		}

		addElement(task, STATUS_TAG, task_.getTaskStatusString());
	}

	private void addElement(Element parent, String elementName, String content) {
		Element element = document.createElement(elementName);

		element.appendChild(document.createTextNode(content));
		parent.appendChild(element);
	}

	private boolean isValidFormat(String fileName_) {
		return (fileName_.endsWith(XML_EXTENSION) && fileName_.substring(0,
				fileName_.length() - 4).matches("^[a-zA-Z0-9]*$")) ? true
						: false;
	}

	private boolean isEmptyFile() {
		BufferedReader br;

		try {
			br = new BufferedReader(new FileReader(fileName));

			if (br.readLine() == null) {
				br.close();

				return true;
			}

			br.close();

			return false;
		} catch (FileNotFoundException e) {
			DisplayAndCommandTextComposite.showToUser(ERROR_INVALID_FILE_NAME);
		}catch (IOException e) {
			DisplayAndCommandTextComposite.showToUser(ERROR_UNABLE_TO_READ_FILE);
		}

		return false;
	}

}
